#include <cerrno>
#include <cstdio>
#include <cstring>
#include <linux/limits.h>
#if __linux
#include <dirent.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif

#include "gt_file.h"
#include "gt_line.h"
#include "gt_list.h"
#include "gt_util.h"

// TODO: this file is not check
#define FOR_LOCK(MUTEX)                                                        \
  for (mutex = &MUTEX; mutex && !pthread_mutex_lock(mutex);                    \
       pthread_mutex_unlock(mutex), mutex = 0)

// ok
static int is_simlink(char *file_path) {
  struct stat filestat;
  lstat(file_path, &filestat);

  return S_ISLNK(filestat.st_mode);
}

// ok
static int is_dir_good(char *dir) {
  return strcmp(dir, ".") != 0 && strcmp(dir, "..") != 0 &&
                 strcmp(dir, ".git") != 0
             ? 1
             : 0;
}

// ok
char *get_file_name(const char *abs_path, char *file_name) {
  char *ret;
  if (strchr(abs_path, '/') == NULL)
    return (char *)abs_path;

  strcpy(file_name, abs_path);

  int len = strlen(file_name);
  if (file_name[len - 1] == '/')
    file_name[len - 1] = '\0';

  ret = file_name;
  if (strrchr(file_name, '/') != NULL)
    ret = strrchr(file_name, '/') + 1;

  return ret;
}

// ok
static void parse_text(search_t *search, const parser_t parser,
                       const char *file_name, int file_size, const char *text,
                       const char *pattern) {
  char *end;
  char *endline;
  int first_occurrence;
  int line_number;
  char *pointer = (char *)text;

  first_occurrence = 1;
  line_number = 1;
  end = pointer + file_size;

  while (true) {
    if (pointer == end)
      break;

    endline = (char *)memchr(pointer, '\n', end - pointer);

    if (endline == NULL)
      break;
    *endline = '\0';

    char *match_begin = parser(search->option, pointer, pattern);
    if (match_begin != NULL) {
      if (first_occurrence) {
        search->result->entries =
            create_file(search->result, (char *)file_name);
        first_occurrence = 0;
      }
      range_t match = {0, 0};
      if (search->option->regexp_option) {
        match.begin = strstr(pointer, match_begin) - pointer;
        match.end = match.begin + strlen(match_begin);
      } else {
        match.begin = match_begin - pointer;
        match.end = match.begin + strlen(search->option->pattern);
      }

      search->result->entries =
          create_line(search->result, pointer, line_number, match);
    }

    *endline = '\n';
    pointer = endline + 1;
    line_number++;
  }
}

// ok
static int is_specific_file(option_t *opt, const char *name) {
  char *name_begins;
  gt_list *pointer = opt->specific_file;

  char file_name[FILENAME_MAX];
  while (pointer) {
    name_begins = get_file_name(name, file_name);
    if (!strcmp(name_begins, pointer->data))
      return 1;

    pointer = pointer->next;
  }

  return 0;
}

// ok
static int is_ignored_file(option_t *opt, const char *name) {
  char *name_begins;
  gt_list *pointer = opt->ignore;

  char file_name[FILENAME_MAX];
  while (pointer) {
    name_begins = get_file_name(name, file_name);
    if (!strcmp(name_begins, pointer->data))
      return 1;
    pointer = pointer->next;
  }

  return 0;
}

// ok
static int is_extension_good(option_t *opt, const char *file) {
  gt_list *ptr;
  ptr = opt->extension;

  while (ptr) {
    if (!strcmp(ptr->data, file + strlen(file) - strlen(ptr->data)))
      return 1;
    ptr = ptr->next;
  }

  return 0;
}

// ok
static int parse_file(search_t *search, const parser_t parser, const char *file,
                      const char *pattern) {
  int f;
  char *ptr;
  char *start;
  struct stat sb;
  errno = 0;

  f = open(file, O_RDONLY);
  if (f < 0)
    return -1;
  if (fstat(f, &sb) < 0) {
    close(f);
    return -1;
  }

  ptr = (char *)mmap(0, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, f, 0);
  start = ptr;
  if (ptr == MAP_FAILED) {
    close(f);
    return -1;
  }

  close(f);

  parse_text(search, parser, file, sb.st_size, start, pattern);

  if (munmap(start, sb.st_size) < 0)
    return -1;

  return 0;
}

// ok
static void lookup_file(search_t *search, const parser_t parser,
                        const char *file, const char *pattern) {
  errno = 0;
  pthread_mutex_t *mutex;

  if (is_ignored_file(search->option, file) && !search->option->raw_option)
    return;

  if (search->option->raw_option) {
    FOR_LOCK(search->data_mutex) parse_file(search, parser, file, pattern);
    return;
  }

  if (is_specific_file(search->option, file)) {
    FOR_LOCK(search->data_mutex)
    parse_file(search, parser, file, pattern);
    return;
  }

  if (is_extension_good(search->option, file)) {
    FOR_LOCK(search->data_mutex) parse_file(search, parser, file, pattern);
    return;
  }
}

// ok
static void lookup_directory(search_t *search, const parser_t parser,
                             const char *dir, const char *pattern) {
  DIR *dp;
  if (is_ignored_file(search->option, dir))
    return;

  dp = opendir(dir);

  if (!dp)
    return;

  while (true) {
    dirent *ep;
    ep = readdir(dp);

    if (!ep)
      break;

    if (!(ep->d_type & DT_DIR)) {
      char file_path[PATH_MAX];
      snprintf(file_path, PATH_MAX, "%s/%s", dir, ep->d_name);

      if (!is_simlink(file_path)) {
        lookup_file(search, parser, file_path, pattern);
      }

      if (ep->d_type & DT_DIR && is_dir_good(ep->d_name)) {
        char path_dir[PATH_MAX] = "";
        snprintf(path_dir, PATH_MAX, "%s/%s", path_dir, pattern);
        lookup_directory(search, parser, path_dir, pattern);
      }
    }

    closedir(dp);
  }
}

// ok
void do_grep_search(search_t *search) {
  parser_t parser = (parser_t)from_options_to_parser(search->option);

  lookup_directory(search, parser, search->option->directory,
                   search->option->pattern);
}